/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package clasificador.redneuronal;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import utils.Logger;
import clasificador.ClasificadorEntrenado;
import clasificador.redneuronal.neurona.Neurona;
import clasificador.redneuronal.neurona.Signal;
import datos.Atributo;
import datos.AtributoFinito;
import datos.Dato;
import datos.MuestraClasificada;

/**
 *
 * @author e237573
 */
public abstract class RedNeuronal<Clase> extends ClasificadorEntrenado<Clase> {

    private Set<InputSignal> inputs;
    private Map<Clase, Neurona> outputs;
    private Set<OutputError> outputErrors;
    private Set<Neurona> neuronas;
    protected int nEpochs;
    protected double learningCoefficient;
    protected double decay;

    public RedNeuronal(MuestraClasificada<Clase> datosEntrenamiento, Map<String, Object> parametros) {
        super(datosEntrenamiento, parametros);
    }

    protected class InputSignal extends Signal<Double> {

        private Atributo<Double> atributo;

        protected InputSignal(Atributo<Double> atributo) {
            this.atributo = atributo;
        }

        protected void setDatoActual(Dato datoActual) {
            this.value = datoActual.getAtributo(atributo);
            /*            this.setChanged();
             this.notifyObservers();*/
        }

        @Override
        public void recalculate() {
        }
    }

    protected class OutputError extends Signal<Double> {

        private Clase clase;
        private Double realValue;
        private Neurona output;

        protected OutputError(Neurona output, Clase clase) {
            this.output = output;
            this.clase = clase;
            output.linkForward(this);
            this.realValue = 0.0;
        }

        @Override
        public void recalculate() {
            this.value = output.outputSignal.getValue() - this.realValue;
        }

        protected void setRealValue(Clase clase) {
            if (this.clase.equals(clase)) {
                this.realValue = 1.0;
            } else {
                this.realValue = 0.0;
            }
            /*            this.setChanged();
             this.notifyObservers(); */
        }
    }

    /* Un output para cada posible valor de la clase */
    protected void createOutputs(MuestraClasificada<Clase> datosEntrenamiento, Map<Clase, Neurona> outputs) {
        //Logger.info("Creando salidas.");
        AtributoFinito<Clase> clase = (AtributoFinito<Clase>) datosEntrenamiento.getClase();

        this.outputs = new HashMap<>(clase.getRango().size());
        this.outputErrors = new HashSet<>(clase.getRango().size());

        for (Clase c : clase.getRango()) {
            //Logger.info("Creando salida para clase " + c);
            Neurona n = outputs.get(c);
            this.outputs.put(c, n);
            OutputError error = new OutputError(n, c);
            this.outputErrors.add(error);
        }
    }

    /* Un input para cada atributo salvo la clase. Deben ser todos numericos. */
    protected Set<InputSignal> createInputs(MuestraClasificada<Clase> datosEntrenamiento) {
        //Logger.info("Creando entradas.");
        Atributo<Clase> clase = datosEntrenamiento.getClase();

        this.inputs = new HashSet<>(datosEntrenamiento.getAtributos().size() - 1);
        for (Atributo<?> a : datosEntrenamiento.getAtributos()) {
            if (!(a.equals(clase))) {
                //Logger.info("Creando entrada para atributo " + a);
                this.inputs.add(new InputSignal((Atributo<Double>) a));
            }
        }

        return this.inputs;
    }

    @Override
    protected void entrenar(MuestraClasificada<Clase> datosEntrenamiento) {
        //Logger.info("Comenzando entrenamiento");

        //Logger.info("Generando red...");
        this.neuronas = generarRed(datosEntrenamiento);
        //Logger.info("Red generada.");

        //Logger.info("Learning coefficient: " + learningCoefficient);
        double currentLearningCoefficient = learningCoefficient;

        //Logger.info("Inicializando neuronas...");
        for (Neurona n : this.neuronas) {
            n.init();
        }
        //Logger.info("Neuronas inicializadas.");


        for (int epoch = 1; epoch <= nEpochs; epoch++) {
            //Logger.info("Epoca numero " + epoch);
            for (Dato dato : datosEntrenamiento.getDatos()) {
                //Logger.info("&&&NUEVO DATO&&&");
                //Logger.info("Dato: " + dato);
                Clase clase = dato.getAtributo(datosEntrenamiento.getClase());

                /* Asignamos el valor a las entradas */
                for (InputSignal in : inputs) {
                    in.setDatoActual(dato);
                    //Logger.info("New input: " + in.getValue());
                }

                for (Neurona n : this.neuronas) {
                    n.invalidate();
                }
                for (OutputError o : this.outputErrors) {
                    o.invalidate();
                }
                for (InputSignal in : inputs) {
                    in.invalidate();
                }

                /* Calcular el error de las salidas */
                for (OutputError outputError : outputErrors) {
                    outputError.setRealValue(clase);
                    //Logger.info("Output error: " + outputError.getValue());
                }

                //Logger.info("Entrenando neuronas para el dato actual.");

                /* Las neuronas se recorren en un orden al azar, pero la API de neurona, incluyendo el sistema de senyales, aseguran
                 * que siempre se calculan los valores una sola vez y siempre se han calculado los valores necesarios antes de calcular los siguientes
                 * valores. En realidad, es como si se calcularan de delante hacia atras y luego de atras hacia delante.
                 */
                for (Neurona n : neuronas) {
                    //Logger.info("Entrenando neurona...");
                    n.train(currentLearningCoefficient);
                }

                /* TODO: Actualizar pesos */
                /*for(Neurona n: neuronas)
                 {
                 n.applyTraining(); 
                 }*/
            }
            currentLearningCoefficient *= decay;
        }
    }

    @Override
    protected void inicializarParametros(Map<String, Object> parametros) {
        nEpochs = 100;
        learningCoefficient = 0.1;
        decay = 1.0;

        if (parametros.get("trainingEpochs") != null) {
            nEpochs = (Integer) parametros.get("trainingEpochs");
        }
        if (parametros.get("learningCoefficient") != null) {
            learningCoefficient = (Double) parametros.get("learningCoefficient");
        }
        if (parametros.get("learningCoefficientDecay") != null) {
            decay = (Double) parametros.get("learningCoefficientDecay");
        }
    }

    @Override
    public Clase clasificar(Dato d) {
        //Logger.info("Clasificando dato: " + d);
        Clase claseActual = null;
        double valorActual = -Double.MAX_VALUE;

        for (InputSignal in : inputs) {
            in.setDatoActual(d);
        }

        for (Neurona n : this.neuronas) {
            n.invalidate();
        }
        for (OutputError o : this.outputErrors) {
            o.invalidate();
        }
        for (InputSignal in : inputs) {
            in.invalidate();
        }

        for (Map.Entry<Clase, Neurona> nsalida : outputs.entrySet()) {
            //Logger.info("Calculando probabilidad para clase " + nsalida.getKey());
            double valor = nsalida.getValue().outputSignal.getValue();
            //Logger.info("Probabilidad: " + valor);
            if (claseActual == null || valor > valorActual) {
                //Logger.warning("La clase " + nsalida.getKey() + " tiene mas probabilidad (" + valor + ") que la clase " + claseActual + "(" + valorActual + ")");
                claseActual = nsalida.getKey();
                valorActual = valor;
            }
        }

        //Logger.info("Clase a clasificar: " + claseActual);
        return claseActual;
    }

    /* Esta funcion debera generar la red a entrenar, creando todas las neuronas necesarias, con sus conexiones, as� como seleccionando
     * los inputs y los outputs. Para ello, debera hacer uso de las funciones createInputs y createOutputs con los datos adecuados.
     * 
     * Se le pasa la muestra, en primer lugar para poder escoger los inputs y outputs correctamente, y tambien,
     * por si en algun caso se escogiera generar redes distintas en funcion de la muestra.
     * 
     * Esta funcion se llama durante el entrenamiento del clasificador, al comienzo.
     */
    protected abstract Set<Neurona> generarRed(MuestraClasificada<Clase> datosEntrenamiento);
}
